(in-package #:lilu-dmenu)

;;;
;;;
;;;

(defclass simple-dmenu-program-application (lilu-app:simple-program-application-mixin
                                            lilu-app:application)
  ()
  (:default-initargs
   :tags '(:dmenu)))

(defun register-simple-dmenu-program (name program version-option version-script make-args-fn)
  (lilu-app:register-application (make-instance 'simple-dmenu-program-application
                                                :name (string name)
                                                :program program
                                                :version-option version-option
                                                :version-script version-script
                                                :make-args-fn make-args-fn)))

(defmethod run-dynamic-menu ((app simple-dmenu-program-application) prompt sequence
                             &rest args &key item-fn only-match-p force-only-match-p
                                          case-sensitive-p error-p)
  (declare (ignorable item-fn only-match-p force-only-match-p case-sensitive-p error-p))
  (let ((cmd (apply #'lilu-app:make-simple-program-shell-command app prompt sequence
                    (alexandria:remove-from-plist args :item-fn :only-match-p :error-p))))
    (let ((shell (uiop:launch-program cmd :input :stream :output :stream :error-output :output)))
      (format (uiop:process-info-input shell) "~{~A~%~}" (map 'list item-fn sequence))
      (force-output (uiop:process-info-input shell))
      (close (if (typep (uiop:process-info-input shell) 'two-way-stream)
                 (two-way-stream-output-stream (uiop:process-info-input shell))
                 (uiop:process-info-input shell)))
      (let ((ret (read-line (uiop:process-info-output shell) nil)))
        (uiop:close-streams shell)
        (let ((exit-code (uiop:wait-process shell)))
          (unless (or (eql 0 exit-code)
                      (and (eql 1 exit-code)
                           (not ret)))
            (error "error (exit code: ~A) running ~A. ~%~A~%"
                   exit-code cmd ret)))
        (let ((element (find ret sequence
                             :key (lambda (x)
                                    (format nil "~A" (funcall item-fn x)))
                             :test #'equal)))
          (if element
              (values element t)
              (values (if only-match-p nil ret) nil)))))))

;;;
;;; utility
;;;

(defmacro %with-parameters (theme (&rest parameters) &body body)
  (let ((m (gensym "m-")))
    `(flet ((,m (p)
              (cdr (assoc p ,theme))))
       (let ,(mapcar #'(lambda (p)
                         (list (intern (symbol-name p)) `(,m ,p)))
              parameters)
         ,@body))))

(defun %build-options (parameter->options theme extra-options)
  (let ((options (funcall parameter->options theme)))
    (if extra-options
        (append extra-options options)
        options)))

;;;
;;; dmenu
;;;

;; Default theme
(defparameter *dmenu-theme* '((:font . nil) ;; e.g. "xft:Terminus:size=10"
                              (:bg . nil) ;; e.g. "#eeffee"
                              (:fg . nil)
                              (:sel-bg . nil)
                              (:sel-fg . nil)
                              (:monitor . nil)
                              (:lines . "0")
                              (:on-bottom . t)))

(defun theme-parameters->dmenu-options (theme)
  (%with-parameters theme (:font :bg :fg :sel-bg :sel-fg :monitor
                           :lines :on-bottom)
    `(,@(when font
          (list "-fn" font))
      ,@(when bg
          (list "-nb" bg))
      ,@(when fg
          (list "-nf" fg))
      ,@(when sel-bg
          (list "-sb" sel-bg))
      ,@(when sel-fg
          (list "-sf" sel-fg))
      ,@(when monitor
          (list "-m" monitor))
      ,@(when lines
          (list "-l" lines))
      ,@(when on-bottom
          (list "-b"))
      )))

(defvar *dmenu-program* (register-simple-dmenu-program
                         :dmenu "dmenu" "-v" "dmenu -v | awk -F '-' '{ print $2}'"
                         #'(lambda (app prompt sequence &key force-only-match-p case-sensitive-p)
                             (declare (ignore app sequence force-only-match-p))
                             (%build-options #'theme-parameters->dmenu-options
                                             *dmenu-theme*
                                             `("-p" ,prompt
                                                    ,@(unless case-sensitive-p
                                                        '("-i")))))))

(defun dmenu (prompt sequence &key (item-fn #'identity) (only-match-p nil)
                                (force-only-match-p nil) (case-sensitive-p nil))
  (dynamic-menu prompt sequence
                :dmenu *dmenu-program*
                :item-fn item-fn
                :only-match-p only-match-p
                :force-only-match-p force-only-match-p
                :case-sensitive-p case-sensitive-p
                :error-p t))
;;;
;;; rofy
;;;

;; Default theme
(defparameter *rofi-theme* '((:font . nil) ;; e.g. "DejaVu 10"
                             (:monitor . nil)
                             (:lines . nil)
                             (:theme . nil) ;; e.g "gruvbox-dark"
                             ))

(defun theme-parameters->rofi-options (theme)
  (%with-parameters theme (:font :monitor :lines :theme)
    `(,@(when theme
          (list "-theme" theme))
      ,@(when monitor
          (list "-m" monitor))
      ,@(when lines
          (list "-l" lines))
      ,@(when font
          (list "-font" font)))))

(defvar *rofi-program* (register-simple-dmenu-program
                        :rofi "rofi" "-v" "rofi -v | awk -F ' ' '{ print $2}'"
                        #'(lambda (app prompt sequence &key force-only-match-p case-sensitive-p)
                            (declare (ignore app sequence))
                            (%build-options #'theme-parameters->rofi-options
                                            *rofi-theme*
                                            `("-dmenu"
                                              "-p" ,prompt
                                              ,@(when force-only-match-p
                                                  '("-only-match"))
                                              ,@(unless case-sensitive-p
                                                  '("-i")))))))

(defun rofi (prompt sequence &key (item-fn #'identity) (force-only-match-p nil)
                               (only-match-p nil) (case-sensitive-p nil))
  (dynamic-menu prompt sequence
                :dmenu *rofi-program*
                :item-fn item-fn
                :only-match-p only-match-p
                :force-only-match-p force-only-match-p
                :case-sensitive-p case-sensitive-p
                :error-p t))

;;;
;;; debugger
;;;

(defun make-dmenu-debugger-hook ()
  (lilu-debugger:make-standard-hook (:dynamic-menu condition me-or-my-encapsulation)
    (let ((selection)
          (*print-case* :downcase))
      (setf selection (lilu-dmenu:dynamic-menu
                       (format nil "select a debugger for: ~A" condition)
                       (set-difference (lilu-app:list-all-applications :debugger t)
                                       (list (string :dynamic-menu)) :test 'equal)))
      (lilu-app:invoke-debugger-hook condition me-or-my-encapsulation
                                     :debugger selection))))

(defvar *dmenu-debugger-hook* (make-dmenu-debugger-hook))

(lilu-debugger:register-simple-debugger :dynamic-menu (make-dmenu-debugger-hook))

(setf *dmenu* (some #'(lambda (app)
                        (when (lilu-app:application-installed-p app)
                          app))
                    (reverse (lilu-app:find-applications :dmenu))))
